
// Copyright (C) 2012 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
using Derm.Scene;

namespace Derm.Xml.Collada
{
	/// <summary>
	/// The scene embodies the entire set of information that can be visualized from the 
	/// contents of a COLLADA resource. The scene element declares the base of the scene 
	/// hierarchy or scene graph. The scene contains elements that comprise much of the 
	/// visual and transformational information content as created by the authoring tools.
	/// </summary>
	public class ColladaDocumentScene : ColladaInstanceLibrary<ColladaVisualScene>
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaDocumentScene.
		/// </summary>
		/// <param name="parent">
		/// The parent element containing this ColladaDocumentScene.
		/// </param>
		public ColladaDocumentScene(ColladaXmlSerializable parent) : base(parent)
		{
			
		}

		#endregion

		#region Document Scene Information

		/// <summary>
		/// The instance_physics_scene element declares the instantiation of a COLLADA physics_scene resource.
		/// The instance_physics_scene element may appear any number of times.
		/// </summary>
		//public List<ColladaInstanceWithExtra> PhysicsScenes = new List<ColladaInstanceWithExtra>();

		/// <summary>
		/// The instance_visual_scene element declares the instantiation of a COLLADA visual_scene resource.
		/// The instance_visual_scene element may only appear once.
		/// </summary>
		public ColladaInstanceVisualScene VisualScene;

		/// <summary>
		/// The extra element may appear any number of times.
		/// </summary>
		public List<ColladaExtra> Extras = new List<ColladaExtra>();

		#endregion

		#region ColladaXmlSerializable Overrides

		/// <summary>
		/// Read an element of the current XML serializable.
		/// </summary>
		/// <param name="elementName">
		/// A <see cref="System.String"/> that specify the element name.
		/// </param>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element. It is positioned at the start element.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is responsible to read the start element of the nested element.
		/// </para>
		/// <para>
		/// This method is resposible to read all children elements till the corresponding end element, without reading it. In the
		/// case this condition is not satisfied, the XML parsing will be aborted.
		/// </para>
		/// </remarks>
		public override void ReadElement(string elementName, XmlReader xmlReader)
		{
			if (elementName == null)
				throw new ArgumentNullException("elementName");
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			if        (elementName == ColladaElementInstancePhysicsScene) {
				//ColladaInstanceWithExtra colladaInstance = new ColladaInstanceWithExtra();
				//colladaInstance.ReadXml(xmlReader);
				//PhysicsScenes.Add(colladaInstance);
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementInstanceVisualScene) {
				VisualScene = new ColladaInstanceVisualScene(this);
				VisualScene.ReadXml(xmlReader);
			} else if (elementName == ColladaElementExtra) {
				ColladaExtra colladaExtra = new ColladaExtra(this);
				colladaExtra.ReadXml(xmlReader);
				Extras.Add(colladaExtra);
			} else
				XmlHelper.SkipXmlElement(xmlReader);
		}

		/// <summary>
		/// Collada 'COLLADA/scene' child element name (1.4).
		/// </summary>
		private const string ColladaElementInstancePhysicsScene = "instance_physics_scene";

		/// <summary>
		/// Collada 'COLLADA/scene' child element name (1.4).
		/// </summary>
		private const string ColladaElementInstanceVisualScene = "instance_visual_scene";

		/// <summary>
		/// Collada 'COLLADA/scene' child element name (1.4).
		/// </summary>
		private const string ColladaElementExtra = "extra";

		#endregion
	}

	/// <summary>
	/// The COLLADA element declares the root of the document that comprises some of the content in the COLLADA schema.
	/// </summary>
	[XmlRoot("COLLADA")]
	public class ColladaDocument : ColladaXmlSerializable
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaDocument.
		/// </summary>
		public ColladaDocument() : base(null)
		{
			
		}

		#endregion

		#region Collada Document Information

		/// <summary>
		/// The version attribute is the COLLADA schema revision with which the instance document conforms.
		/// </summary>
		public ColladaVersionType Version;

		/// <summary>
		/// The COLLADA element must contain an asset element.
		/// </summary>
		public ColladaAsset Asset;

		/// <summary>
		/// The COLLADA element may contain any number of library_animations elements.
		/// </summary>
		public List<ColladaLibraryAnimations> LibraryAnimations = new List<ColladaLibraryAnimations>();

		/// <summary>
		/// The COLLADA element may contain any number of library_animation_clips elements.
		/// </summary>
		public List<ColladaLibraryAnimationClips> LibraryAnimationClips = new List<ColladaLibraryAnimationClips>();

		/// <summary>
		/// The COLLADA element may contain any number of library_controllerss elements.
		/// </summary>
		public List<ColladaLibraryControllers> LibraryControllers = new List<ColladaLibraryControllers>();

		/// <summary>
		/// The COLLADA element may contain any number of library_cameras elements.
		/// </summary>
		public List<ColladaLibraryCameras> LibraryCameras = new List<ColladaLibraryCameras>();

		/// <summary>
		/// The COLLADA element may contain any number of library_geometries elements.
		/// </summary>
		public List<ColladaLibraryGeometries> LibraryGeometries = new List<ColladaLibraryGeometries>();

		/// <summary>
		/// The COLLADA element may contain any number of library_effects elements.
		/// </summary>
		public List<ColladaLibraryEffects> LibraryEffects = new List<ColladaLibraryEffects>();

		/// <summary>
		/// The COLLADA element may contain any number of library_force_fields elements.
		/// </summary>
		public List<ColladaLibraryForceFields> LibraryForceFields = new List<ColladaLibraryForceFields>();

		/// <summary>
		/// The COLLADA element may contain any number of library_images elements.
		/// </summary>
		public List<ColladaLibraryImages> LibraryImages = new List<ColladaLibraryImages>();

		/// <summary>
		/// The COLLADA element may contain any number of library_lights elements.
		/// </summary>
		public List<ColladaLibraryLights> LibraryLights = new List<ColladaLibraryLights>();

		/// <summary>
		/// The COLLADA element may contain any number of library_materials elements.
		/// </summary>
		public List<ColladaLibraryMaterials> LibraryMaterials = new List<ColladaLibraryMaterials>();

		/// <summary>
		/// The COLLADA element may contain any number of library_nodes elements.
		/// </summary>
		public List<ColladaLibraryNodes> LibraryNodes = new List<ColladaLibraryNodes>();

		/// <summary>
		/// The COLLADA element may contain any number of library_materials elements.
		/// </summary>
		public List<ColladaLibraryPhysicsMaterials> LibraryPhysicsMaterials = new List<ColladaLibraryPhysicsMaterials>();

		/// <summary>
		/// The COLLADA element may contain any number of library_materials elements.
		/// </summary>
		public List<ColladaLibraryPhysicsMaterials> ColladaLibraryPhysicsModels = new List<ColladaLibraryPhysicsMaterials>();

		/// <summary>
		/// The COLLADA element may contain any number of library_materials elements.
		/// </summary>
		public List<ColladaLibraryPhysicsScenes> ColladaLibraryPhysicsScenes = new List<ColladaLibraryPhysicsScenes>();

		/// <summary>
		/// The COLLADA element may contain any number of library_visual_scenes elements.
		/// </summary>
		public List<ColladaLibraryVisualScenes> VisualScenes = new List<ColladaLibraryVisualScenes>();

		/// <summary>
		/// The scene embodies the entire set of information that can be visualized from the contents of a COLLADA resource.
		/// </summary>
		public ColladaDocumentScene Scene;

		/// <summary>
		/// The extra element may appear any number of times.
		/// </summary>
		public List<ColladaExtra> Extras = new List<ColladaExtra>();

		#endregion

		#region Collada Document Special Elements

		internal void RegisterSpecialElement<T>(object colladaElement, string id)
		{
			if (colladaElement == null)
				throw new ArgumentNullException("colladaElement");
			if (id == null)
				throw new ArgumentNullException("id");

			Dictionary<string, object> objectMap;

			// Get the special element objects map for the specified type
			if (mSpecialElements.TryGetValue(typeof(T), out objectMap) == false)
				mSpecialElements[typeof(T)] = objectMap = new Dictionary<string, object>();

			if (objectMap.ContainsKey(id) == true)
				throw new InvalidOperationException(String.Format("instance {0} is duplicated", id));

			objectMap[id] = colladaElement;
		}

		internal T GetSpecialElement<T>(string id)
		{
			if (id == null)
				throw new ArgumentNullException("id");

			// Convert identifier
			if (id.StartsWith("#") == true) {
				// XML fragment name: remove leading '#'
				id = id.Substring(1, id.Length - 1);
			}

			Dictionary<string, object> objectMap;
			object element;

			if (mSpecialElements.TryGetValue(typeof(T), out objectMap) == false)
				throw new InvalidOperationException(String.Format("{0} element with Id '{1}' not found", typeof(T).Name, id));
			if (objectMap.TryGetValue(id, out element) == false)
				throw new InvalidOperationException(String.Format("{0} element with Id '{1}' not found", typeof(T).Name, id));

			return ((T) element);
		}

		/// <summary>
		/// 
		/// </summary>
		private Dictionary<Type, Dictionary<string, object>> mSpecialElements = new Dictionary<Type, Dictionary<string, object>>();

		#endregion

		#region Collada Namespaces

		/// <summary>
		/// Namespace used in COLLADA 1.4.x documents.
		/// </summary>
		public const string Collada140Namespace = "http://www.collada.org/2005/11/COLLADASchema";

		/// <summary>
		/// Namespace used in COLLADA 1.5.0 documents.
		/// </summary>
		public const string Collada150Namespace = "http://www.collada.org/2008/03/COLLADASchema";

		#endregion

		#region Document Importer

		/// <summary>
		/// File where the COLLADA document were loaded.
		/// </summary>
		public string DocumentPath { get { return (mDocumentPath); } }

		/// <summary>
		/// Directory where the COLLADA document were loaded.
		/// </summary>
		public string DocumentDirectory { get { return (mDocumentDirectory); } }

		/// <summary>
		/// File where the COLLADA document were loaded.
		/// </summary>
		private string mDocumentPath;

		/// <summary>
		/// Directory where the COLLADA document were loaded.
		/// </summary>
		private string mDocumentDirectory;

		/// <summary>
		/// Loads a COLLADA document.
		/// </summary>
		/// <param name="path">
		/// A <see cref="System.String"/> that specify the path used for loading the document.
		/// </param>
		/// <returns>
		/// It returns a <see cref="ColladaDocument"/> loaded from <paramref name="path"/>.
		/// </returns>
		public static ColladaDocument Load(string path)
		{
			ColladaDocument colladaDocument;

			if (path == null)
				throw new ArgumentNullException("path");

			sLog.Debug("Load COLLADA document from {0}", path);

			// Deserialize XML document
			using (FileStream sr = new FileStream(path, FileMode.Open, FileAccess.Read)) {
				colladaDocument = (ColladaDocument)XmlHelper.Deserialize(sr, typeof(ColladaDocument), Collada140Namespace);
			}

			// Store filesystem information (used for referencing external resources)
			colladaDocument.mDocumentPath = path;
			colladaDocument.mDocumentDirectory = Path.GetDirectoryName(path);
			// Resolve COLLADA references
			colladaDocument.ResolveReferences(colladaDocument);

			return (colladaDocument);
		}

		#endregion

		#region COLLADA Support

		/// <summary>
		/// Create a <see cref="RenderScene"/> from this <see cref="ColladaDocument"/>.
		/// </summary>
		/// <exception cref="InvalidOperationException">
		/// Exception throw if this colladaDocument does not define all required information.
		/// </exception>
		public RenderScene Create()
		{
			if (Scene == null)
				throw new InvalidOperationException("no Scene");
			if (Scene.VisualScene == null)
				throw new InvalidOperationException("no Scene.VisualScene");

			RenderScene colladaScene = new RenderScene();

			sLog.Debug("Create scene from COLLADA document {0}", DocumentPath);

			// Scene graph
			colladaScene.Hierarchy = Scene.VisualScene.Instance.Create();
			// Ensure camera
			colladaScene.EnsureCamera();

			return (colladaScene);
		}

		#endregion

		#region Logging

		/// <summary>
		/// Logger of this class.
		/// </summary>
		private static readonly ILogger sLog = Log.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region ColladaXmlSerializable Overrides

		/// <summary>
		/// Read the attribute of the current XML serializable.
		/// </summary>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading XML element attributes.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is called only in the case there are effectively attributes on XML element. Don't expect that this
		/// method will be called even when the attributes count is zero. This is very important, since the design doesn't
		/// allow object initialization using this method.
		/// </para>
		/// </remarks>
		public override void ReadAttributes(XmlReader xmlReader)
		{
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			string attribute;

			if ((attribute = xmlReader[ColladaAttributeVersion]) == null)
				throw new InvalidOperationException("no attribute " + ColladaAttributeVersion);
			Version = XmlHelper.ReadXmlEnum<ColladaVersionType>(attribute);
		}

		/// <summary>
		/// Read an element of the current XML serializable.
		/// </summary>
		/// <param name="elementName">
		/// A <see cref="System.String"/> that specify the element name.
		/// </param>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element. It is positioned at the start element.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is responsible to read the start element of the nested element.
		/// </para>
		/// <para>
		/// This method is resposible to read all children elements till the corresponding end element, without reading it. In the
		/// case this condition is not satisfied, the XML parsing will be aborted.
		/// </para>
		/// </remarks>
		public override void ReadElement(string elementName, XmlReader xmlReader)
		{
			if (elementName == null)
				throw new ArgumentNullException("elementName");
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			if        (elementName == ColladaElementAsset) {
				Asset = new ColladaAsset(this);
				Asset.ReadXml(xmlReader);
			} else if (elementName == ColladaElementLibraryAnimations) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryAnimationClips) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryCameras) {
				ColladaLibraryCameras libraryCamera = new ColladaLibraryCameras(this);
				libraryCamera.ReadXml(xmlReader);
				LibraryCameras.Add(libraryCamera);
			} else if (elementName == ColladaElementLibraryControllers) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryGeometries) {
				ColladaLibraryGeometries libraryGeometries = new ColladaLibraryGeometries(this);
				libraryGeometries.ReadXml(xmlReader);
				LibraryGeometries.Add(libraryGeometries);
			} else if (elementName == ColladaElementLibraryEffects) {
				ColladaLibraryEffects libraryEffects = new ColladaLibraryEffects(this);
				libraryEffects.ReadXml(xmlReader);
				LibraryEffects.Add(libraryEffects);
			} else if (elementName == ColladaElementLibraryForceFields) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryImages) {
				ColladaLibraryImages libraryImage = new ColladaLibraryImages(this);
				libraryImage.ReadXml(xmlReader);
				LibraryImages.Add(libraryImage);
			} else if (elementName == ColladaElementLibraryLights) {
				ColladaLibraryLights libraryLight = new ColladaLibraryLights(this);
				libraryLight.ReadXml(xmlReader);
				LibraryLights.Add(libraryLight);
			} else if (elementName == ColladaElementLibraryMaterials) {
				ColladaLibraryMaterials libraryMaterials = new ColladaLibraryMaterials(this);
				libraryMaterials.ReadXml(xmlReader);
				LibraryMaterials.Add(libraryMaterials);
			} else if (elementName == ColladaElementLibraryNodes) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryPhysicsMaterials) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryPhysicsModels) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryPhysicsScenes) {
				XmlHelper.SkipXmlElement(xmlReader);
			} else if (elementName == ColladaElementLibraryVisualScenes) {
				ColladaLibraryVisualScenes libraryVisualScenes = new ColladaLibraryVisualScenes(this);
				libraryVisualScenes.ReadXml(xmlReader);
				VisualScenes.Add(libraryVisualScenes);
			} else if (elementName == ColladaElementScene) {
				Scene = new ColladaDocumentScene(this);
				Scene.ReadXml(xmlReader);
			} else
				base.ReadElement(elementName, xmlReader);
		}

		/// <summary>
		/// Collada 'COLLADA' attribute name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaAttributeVersion = "version";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementAsset = "asset";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryAnimations = "library_animations";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryAnimationClips = "library_animation_clips";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryCameras = "library_cameras";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryControllers = "library_controllers";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryGeometries = "library_geometries";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryEffects = "library_effects";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryForceFields = "library_force_fields";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryImages = "library_images";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryLights = "library_lights";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryMaterials = "library_materials";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryNodes = "library_nodes";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryPhysicsMaterials = "library_physics_materials";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryPhysicsModels = "library_physics_models";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryPhysicsScenes = "library_physics_scenes";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementLibraryVisualScenes = "library_visual_scenes";

		/// <summary>
		/// Collada 'COLLADA' child element name (1.4.1 - 1.5.0).
		/// </summary>
		private const string ColladaElementScene = "scene";

		#endregion
	}
}
